/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ChangeEventPlugin
 */

'use strict'

var EventConstants = require('./EventConstants')
var EventPluginHub = require('./EventPluginHub')
var EventPropagators = require('./EventPropagators')
var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment')
var ReactDOMComponentTree = require('./ReactDOMComponentTree')
var ReactUpdates = require('./ReactUpdates')
var SyntheticEvent = require('./SyntheticEvent')

var getEventTarget = require('./getEventTarget')
var isEventSupported = require('./isEventSupported')
var isTextInputElement = require('./isTextInputElement')
var keyOf = require('fbjs/lib/keyOf')

var topLevelTypes = EventConstants.topLevelTypes

var eventTypes = {
  change: {
    phasedRegistrationNames: {
      bubbled: keyOf({ onChange: null }),
      captured: keyOf({ onChangeCapture: null })
    },
    dependencies: [
      topLevelTypes.topBlur,
      topLevelTypes.topChange,
      topLevelTypes.topClick,
      topLevelTypes.topFocus,
      topLevelTypes.topInput,
      topLevelTypes.topKeyDown,
      topLevelTypes.topKeyUp,
      topLevelTypes.topSelectionChange
    ]
  }
}

/**
 * For IE shims
 */
var activeElement = null
var activeElementInst = null
var activeElementValue = null
var activeElementValueProp = null

/**
 * SECTION: handle `change` event
 */
function shouldUseChangeEvent(elem) {
  var nodeName = elem.nodeName && elem.nodeName.toLowerCase()
  return nodeName === 'select' || (nodeName === 'input' && elem.type === 'file')
}

var doesChangeEventBubble = false
if (ExecutionEnvironment.canUseDOM) {
  // See `handleChange` comment below
  doesChangeEventBubble =
    isEventSupported('change') &&
    (!('documentMode' in document) || document.documentMode > 8)
}

function manualDispatchChangeEvent(nativeEvent) {
  var event = SyntheticEvent.getPooled(
    eventTypes.change,
    activeElementInst,
    nativeEvent,
    getEventTarget(nativeEvent)
  )
  EventPropagators.accumulateTwoPhaseDispatches(event)

  // If change and propertychange bubbled, we'd just bind to it like all the
  // other events and have it go through ReactBrowserEventEmitter. Since it
  // doesn't, we manually listen for the events and so we have to enqueue and
  // process the abstract event manually.
  //
  // Batching is necessary here in order to ensure that all event handlers run
  // before the next rerender (including event handlers attached to ancestor
  // elements instead of directly on the input). Without this, controlled
  // components don't work properly in conjunction with event bubbling because
  // the component is rerendered and the value reverted before all the event
  // handlers can run. See https://github.com/facebook/react/issues/708.
  ReactUpdates.batchedUpdates(runEventInBatch, event)
}

function runEventInBatch(event) {
  EventPluginHub.enqueueEvents(event)
  EventPluginHub.processEventQueue(false)
}

function startWatchingForChangeEventIE8(target, targetInst) {
  activeElement = target
  activeElementInst = targetInst
  activeElement.attachEvent('onchange', manualDispatchChangeEvent)
}

function stopWatchingForChangeEventIE8() {
  if (!activeElement) {
    return
  }
  activeElement.detachEvent('onchange', manualDispatchChangeEvent)
  activeElement = null
  activeElementInst = null
}

function getTargetInstForChangeEvent(topLevelType, targetInst) {
  if (topLevelType === topLevelTypes.topChange) {
    return targetInst
  }
}
function handleEventsForChangeEventIE8(topLevelType, target, targetInst) {
  if (topLevelType === topLevelTypes.topFocus) {
    // stopWatching() should be a noop here but we call it just in case we
    // missed a blur event somehow.
    stopWatchingForChangeEventIE8()
    startWatchingForChangeEventIE8(target, targetInst)
  } else if (topLevelType === topLevelTypes.topBlur) {
    stopWatchingForChangeEventIE8()
  }
}

/**
 * SECTION: handle `input` event
 */
var isInputEventSupported = false
if (ExecutionEnvironment.canUseDOM) {
  // IE9 claims to support the input event but fails to trigger it when
  // deleting text, so we ignore its input events.
  // IE10+ fire input events to often, such when a placeholder
  // changes or when an input with a placeholder is focused.
  isInputEventSupported =
    isEventSupported('input') &&
    (!('documentMode' in document) || document.documentMode > 11)
}

/**
 * (For IE <=11) Replacement getter/setter for the `value` property that gets
 * set on the active element.
 */
var newValueProp = {
  get: function () {
    return activeElementValueProp.get.call(this)
  },
  set: function (val) {
    // Cast to a string so we can do equality checks.
    activeElementValue = '' + val
    activeElementValueProp.set.call(this, val)
  }
}

/**
 * (For IE <=11) Starts tracking propertychange events on the passed-in element
 * and override the value property so that we can distinguish user events from
 * value changes in JS.
 */
function startWatchingForValueChange(target, targetInst) {
  activeElement = target
  activeElementInst = targetInst
  activeElementValue = target.value
  activeElementValueProp = Object.getOwnPropertyDescriptor(
    target.constructor.prototype,
    'value'
  )

  // Not guarded in a canDefineProperty check: IE8 supports defineProperty only
  // on DOM elements
  Object.defineProperty(activeElement, 'value', newValueProp)
  if (activeElement.attachEvent) {
    activeElement.attachEvent('onpropertychange', handlePropertyChange)
  } else {
    activeElement.addEventListener(
      'propertychange',
      handlePropertyChange,
      false
    )
  }
}

/**
 * (For IE <=11) Removes the event listeners from the currently-tracked element,
 * if any exists.
 */
function stopWatchingForValueChange() {
  if (!activeElement) {
    return
  }

  // delete restores the original property definition
  delete activeElement.value

  if (activeElement.detachEvent) {
    activeElement.detachEvent('onpropertychange', handlePropertyChange)
  } else {
    activeElement.removeEventListener(
      'propertychange',
      handlePropertyChange,
      false
    )
  }

  activeElement = null
  activeElementInst = null
  activeElementValue = null
  activeElementValueProp = null
}

/**
 * (For IE <=11) Handles a propertychange event, sending a `change` event if
 * the value of the active element has changed.
 */
function handlePropertyChange(nativeEvent) {
  if (nativeEvent.propertyName !== 'value') {
    return
  }
  var value = nativeEvent.srcElement.value
  if (value === activeElementValue) {
    return
  }
  activeElementValue = value

  manualDispatchChangeEvent(nativeEvent)
}

/**
 * If a `change` event should be fired, returns the target's ID.
 */
function getTargetInstForInputEvent(topLevelType, targetInst) {
  if (topLevelType === topLevelTypes.topInput) {
    // In modern browsers (i.e., not IE8 or IE9), the input event is exactly
    // what we want so fall through here and trigger an abstract event
    return targetInst
  }
}

function handleEventsForInputEventIE(topLevelType, target, targetInst) {
  if (topLevelType === topLevelTypes.topFocus) {
    // In IE8, we can capture almost all .value changes by adding a
    // propertychange handler and looking for events with propertyName
    // equal to 'value'
    // In IE9-11, propertychange fires for most input events but is buggy and
    // doesn't fire when text is deleted, but conveniently, selectionchange
    // appears to fire in all of the remaining cases so we catch those and
    // forward the event if the value has changed
    // In either case, we don't want to call the event handler if the value
    // is changed from JS so we redefine a setter for `.value` that updates
    // our activeElementValue variable, allowing us to ignore those changes
    //
    // stopWatching() should be a noop here but we call it just in case we
    // missed a blur event somehow.
    stopWatchingForValueChange()
    startWatchingForValueChange(target, targetInst)
  } else if (topLevelType === topLevelTypes.topBlur) {
    stopWatchingForValueChange()
  }
}

// For IE8 and IE9.
function getTargetInstForInputEventIE(topLevelType, targetInst) {
  if (
    topLevelType === topLevelTypes.topSelectionChange ||
    topLevelType === topLevelTypes.topKeyUp ||
    topLevelType === topLevelTypes.topKeyDown
  ) {
    // On the selectionchange event, the target is just document which isn't
    // helpful for us so just check activeElement instead.
    //
    // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
    // propertychange on the first input event after setting `value` from a
    // script and fires only keydown, keypress, keyup. Catching keyup usually
    // gets it and catching keydown lets us fire an event for the first
    // keystroke if user does a key repeat (it'll be a little delayed: right
    // before the second keystroke). Other input methods (e.g., paste) seem to
    // fire selectionchange normally.
    if (activeElement && activeElement.value !== activeElementValue) {
      activeElementValue = activeElement.value
      return activeElementInst
    }
  }
}

/**
 * SECTION: handle `click` event
 */
function shouldUseClickEvent(elem) {
  // Use the `click` event to detect changes to checkbox and radio inputs.
  // This approach works across all browsers, whereas `change` does not fire
  // until `blur` in IE8.
  return (
    elem.nodeName &&
    elem.nodeName.toLowerCase() === 'input' &&
    (elem.type === 'checkbox' || elem.type === 'radio')
  )
}

function getTargetInstForClickEvent(topLevelType, targetInst) {
  if (topLevelType === topLevelTypes.topClick) {
    return targetInst
  }
}

/**
 * This plugin creates an `onChange` event that normalizes change events
 * across form elements. This event fires at a time when it's possible to
 * change the element's value without seeing a flicker.
 *
 * Supported elements are:
 * - input (see `isTextInputElement`)
 * - textarea
 * - select
 */
var ChangeEventPlugin = {
  eventTypes: eventTypes,

  extractEvents: function (
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget
  ) {
    var targetNode = targetInst
      ? ReactDOMComponentTree.getNodeFromInstance(targetInst)
      : window

    var getTargetInstFunc, handleEventFunc
    if (shouldUseChangeEvent(targetNode)) {
      if (doesChangeEventBubble) {
        getTargetInstFunc = getTargetInstForChangeEvent
      } else {
        handleEventFunc = handleEventsForChangeEventIE8
      }
    } else if (isTextInputElement(targetNode)) {
      if (isInputEventSupported) {
        getTargetInstFunc = getTargetInstForInputEvent
      } else {
        getTargetInstFunc = getTargetInstForInputEventIE
        handleEventFunc = handleEventsForInputEventIE
      }
    } else if (shouldUseClickEvent(targetNode)) {
      getTargetInstFunc = getTargetInstForClickEvent
    }

    if (getTargetInstFunc) {
      var inst = getTargetInstFunc(topLevelType, targetInst)
      if (inst) {
        var event = SyntheticEvent.getPooled(
          eventTypes.change,
          inst,
          nativeEvent,
          nativeEventTarget
        )
        event.type = 'change'
        EventPropagators.accumulateTwoPhaseDispatches(event)
        return event
      }
    }

    if (handleEventFunc) {
      handleEventFunc(topLevelType, targetNode, targetInst)
    }
  }
}

module.exports = ChangeEventPlugin
